{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Week 2 Day 2\n",
    "\n",
    "Our first Agentic Framework project!!\n",
    "\n",
    "Prepare yourself for something ridiculously easy.\n",
    "\n",
    "We're going to build a simple Agent system for generating cold sales outreach emails:\n",
    "1. Agent workflow\n",
    "2. Use of tools to call functions\n",
    "3. Agent collaboration via Tools and Handoffs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Before we start - some setup:\n",
    "\n",
    "\n",
    "Please visit Sendgrid at: https://sendgrid.com/\n",
    "\n",
    "(Sendgrid is a Twilio company for sending emails.)\n",
    "\n",
    "If SendGrid gives you problems, see the alternative implementation using \"Resend Email\" in community_contributions/2_lab2_with_resend_email\n",
    "\n",
    "Please set up an account - it's free! (at least, for me, right now).\n",
    "\n",
    "Once you've created an account, click on:\n",
    "\n",
    "Settings (left sidebar) >> API Keys >> Create API Key (button on top right)\n",
    "\n",
    "Copy the key to the clipboard, then add a new line to your .env file:\n",
    "\n",
    "`SENDGRID_API_KEY=xxxx`\n",
    "\n",
    "And also, within SendGrid, go to:\n",
    "\n",
    "Settings (left sidebar) >> Sender Authentication >> \"Verify a Single Sender\"  \n",
    "and verify that your own email address is a real email address, so that SendGrid can send emails for you.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import load_dotenv\n",
    "from agents import Agent, Runner, trace, function_tool\n",
    "from openai.types.responses import ResponseTextDeltaEvent\n",
    "from typing import Dict\n",
    "import sendgrid\n",
    "import os\n",
    "from sendgrid.helpers.mail import Mail, Email, To, Content\n",
    "import asyncio\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's just check emails are working for you\n",
    "\n",
    "def send_test_email():\n",
    "    sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY'))\n",
    "    from_email = Email(\"lchanio@echyperion.com\")  # Change to your verified sender\n",
    "    to_email = To(\"lchanio@echyperion.com\")  # Change to your recipient\n",
    "    content = Content(\"text/plain\", \"This is an important test email\")\n",
    "    mail = Mail(from_email, to_email, \"[SendGrid-dev] Test email\", content).get()\n",
    "    response = sg.client.mail.send.post(request_body=mail)\n",
    "    print(response.status_code)\n",
    "\n",
    "send_test_email()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Did you receive the test email\n",
    "\n",
    "If you get a 202, then you're good to go!\n",
    "\n",
    "#### Certificate error\n",
    "\n",
    "If you get an error SSL: CERTIFICATE_VERIFY_FAILED then students Chris S and Oleksandr K have suggestions:  \n",
    "First run this: `!uv pip install --upgrade certifi`  \n",
    "Next, run this:\n",
    "```python\n",
    "import certifi\n",
    "import os\n",
    "os.environ['SSL_CERT_FILE'] = certifi.where()\n",
    "```\n",
    "\n",
    "#### Other errors or no email\n",
    "\n",
    "If there are other problems, you'll need to check your API key and your verified sender email address in the SendGrid dashboard\n",
    "\n",
    "Or use the alternative implementation using \"Resend Email\" in community_contributions/2_lab2_with_resend_email\n",
    "\n",
    "(Or - you could always replace the email sending code below with a Pushover call, or something to simply write to a flat file)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1: Agent workflow"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Add information about the product in the system prompt. This will be used to write the e-mails.\n",
    "with open('ComplAI_brochure.md','r') as file:\n",
    "    brochure_text = file.read()\n",
    "\n",
    "instructions1 = \"You are a sales agent working for ComplAI, \\\n",
    "a company that provides a SaaS tool for ensuring SOC2 compliance and preparing for audits, powered by AI. \\\n",
    "You write professional, serious cold emails. Here is some information about the product:\\n\" + brochure_text\n",
    "\n",
    "instructions2 = \"You are a humorous, engaging sales agent working for ComplAI, \\\n",
    "a company that provides a SaaS tool for ensuring SOC2 compliance and preparing for audits, powered by AI. \\\n",
    "You write witty, engaging cold emails that are likely to get a response. Here is some information about the product:\\n\" + brochure_text\n",
    "\n",
    "instructions3 = \"You are a busy sales agent working for ComplAI, \\\n",
    "a company that provides a SaaS tool for ensuring SOC2 compliance and preparing for audits, powered by AI. \\\n",
    "You write concise, to the point cold emails. Here is some information about the product:\\n\" + brochure_text\n",
    "\n",
    "sales_agent_instructions = [instructions1, instructions2, instructions3] # we can iterate over the list later on\n",
    "sales_agent_names = [\"Professional Sales Agent\", \"Humorous Sales Agent\", \"Busy Sales Agent\"]\n",
    "sales_agents_metadata = zip(sales_agent_names, sales_agent_instructions)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_agents = []\n",
    "for agent_name, instruction in list(sales_agents_metadata):\n",
    "        sales_agents.append(Agent(\n",
    "                name=agent_name,\n",
    "                instructions=instruction,\n",
    "                model=\"gpt-4o-mini\"\n",
    "        ))\n",
    "\n",
    "print(f\"Initializsed {len(sales_agents)} agents\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Read e-mails from a contact list\n",
    "\n",
    "Let's update the agents to create e-mails for specific people from a contact list.\n",
    "We will provide more context to the workflow in the form of a product brochure and recipients.\n",
    "The agents will craft a personalized e-mail according to each recipient role. \n",
    "\n",
    "In the workflow, we will first select the appropriate recipients. \n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "contacts = pd.read_csv('contact_list.csv')\n",
    "print(contacts)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test it\n",
    "for keys,contact in contacts.iloc[5:7,:].iterrows(): # try it with the two C-level officers in the list\n",
    "    contact_name = contact[['Name']]\n",
    "    contact_title = contact['Title']\n",
    "    for sales_agent in sales_agents:\n",
    "        result = Runner.run_streamed(sales_agent, input=f\"Write a cold sales email addressed to {contact_name} who is a {contact_title}. Adjust the content according to their position in their company.\")\n",
    "        async for event in result.stream_events():\n",
    "            if event.type == \"raw_response_event\" and isinstance(event.data, ResponseTextDeltaEvent):\n",
    "                print(event.data.delta, end=\"\", flush=True)\n",
    "        print(\"-\"*30)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Parallel execution wrapper functions\n",
    "1. Wrapped agent execution in an async function that collects the full streamed response\n",
    "2. Used asyncio.gather(*tasks) to run all agents in parallel for each contact\n",
    "3. Maintained streaming within each agent while parallelizing across agents\n",
    "4. Preserved tracing structure\n",
    "5. Coroutines to handle parallel execution for a single contact, or fully parallel (all agents work in parallel at all contacts)\n",
    "\n",
    "This was created as an exercise in parallellization. For a large number of e-mails we can paralellise the e-mail generation and the selections in the next step. However it requires some additional orchestration and synchronization logic to group the e-mails together, as we don't know in advance when the three e-mails for a given customer will be returned. \n",
    "\n",
    "So, while I created the fully parallel execution function, I opted for a tamer version that just executes the 3 writers for a given customer in parallel. I may return to this and create a multi-threaded version (one thread per customer) to make it faster. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_picker = Agent(\n",
    "    name=\"sales_picker\",\n",
    "    instructions=\"You pick the best cold sales email from the given options. \\\n",
    "Imagine you are a customer and pick the one you are most likely to respond to. \\\n",
    "Do not give an explanation; reply with the selected email number only.\",\n",
    "    model=\"gpt-4o-mini\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def run_agent_for_contact(sales_agent, contact_name, contact_title):\n",
    "    \"\"\"Run a single agent for a contact and collect the full response\n",
    "    \n",
    "    Args:\n",
    "        sales_agent: The agent object that will generate the sales email\n",
    "        contact_name (str): The name of the contact to address the email to\n",
    "        contact_title (str): The job title of the contact\n",
    "        \n",
    "    Returns:\n",
    "        str: The complete generated sales email text\n",
    "        \n",
    "    The function streams the agent's response and accumulates the text deltas\n",
    "    into a single string containing the full email content.\n",
    "    \"\"\"\n",
    "    full_response = \"\"\n",
    "    result = Runner.run_streamed(sales_agent, input=f\"Write a cold sales email addressed to {contact_name} who is a {contact_title}. Adjust the content according to their position in their company.\")\n",
    "    \n",
    "    async for event in result.stream_events():\n",
    "        if event.type == \"raw_response_event\" and isinstance(event.data, ResponseTextDeltaEvent):\n",
    "            full_response += event.data.delta\n",
    "    \n",
    "    return full_response\n",
    "\n",
    "async def process_contact_parallel(contact, sales_agents):\n",
    "    \"\"\"Process a single contact with all agents in parallel\n",
    "    Args:\n",
    "        contact (pd.DataFrame): A DataFrame containing contact information.\n",
    "        sales_agents (list): A list of Agent objects representing sales agents.\n",
    "    Returns:\n",
    "        list: A list of results from running the agents for the contact.\n",
    "    \"\"\"    \n",
    "    \n",
    "    contact_name = contact['Name']\n",
    "    contact_title = contact['Title']\n",
    "    \n",
    "    # Create tasks for all agents for this contact\n",
    "    tasks = [\n",
    "        run_agent_for_contact(agent, contact_name, contact_title) \n",
    "        for agent in sales_agents\n",
    "    ]\n",
    "    \n",
    "    # Run all agents in parallel for this contact\n",
    "    with trace(f\"Parallel cold emails for {contact_name}\"):\n",
    "        results = await asyncio.gather(*tasks)\n",
    "    \n",
    "    return results\n",
    "\n",
    "async def execute_fully_parallel(contacts, sales_agents):\n",
    "    \"\"\" Execute all e-mail composition agents in parallel \n",
    "    (3 agents per contact for each contact) \n",
    "    \n",
    "    Args:\n",
    "        contacts (pd.DataFrame): A DataFrame containing contact information.\n",
    "        sales_agents (list): A list of Agent objects representing sales agents.\n",
    "    Returns:\n",
    "        None\n",
    "    \"\"\"\n",
    "\n",
    "    all_tasks = []\n",
    "    \n",
    "    for keys, contact in contacts.iloc[5:7, :].iterrows():\n",
    "        contact_name = contact['Name']\n",
    "        contact_title = contact['Title']\n",
    "        \n",
    "        for agent in sales_agents:\n",
    "            task = run_agent_for_contact(agent, contact_name, contact_title)\n",
    "            all_tasks.append((contact_name, agent, task))\n",
    "    \n",
    "    # Run everything in parallel\n",
    "    with trace(\"All parallel cold emails\"):\n",
    "        results = await asyncio.gather(*[task for _, _, task in all_tasks])\n",
    "    \n",
    "    # Process results\n",
    "    for i, (contact_name, agent, _) in enumerate(all_tasks):\n",
    "        print(f\"\\n{contact_name} - Agent {agent.name}: {results[i]}\")\n",
    "\n",
    "\n",
    "async def select_best_email(emails, picker_agent):\n",
    "    email_txt = \"These are the cold sales e-mails:\\n\".join(emails)\n",
    "\n",
    "    best = await Runner.run(picker_agent, email_txt)\n",
    "    # print(f\"Best sales email:\\n{best.final_output}\")\n",
    "    return best.final_output\n",
    "\n",
    "# Execute one customer at a time with parallel composition agents\n",
    "async def execute(contacts, sales_agents):\n",
    "    \"\"\"Execute email generation for each contact using multiple sales agents in parallel\n",
    "    \n",
    "    Args:\n",
    "        contacts (pd.DataFrame): DataFrame containing contact information including Name and Title\n",
    "        sales_agents (list): List of Agent objects that will generate sales emails\n",
    "        \n",
    "    Returns:\n",
    "        pd.DataFrame: The contacts DataFrame with a new Email_Text column containing the best \n",
    "                     generated email for each contact\n",
    "                     \n",
    "    For each contact, this function:\n",
    "    1. Generates multiple email versions in parallel using the sales agents\n",
    "    2. Selects the best email using a picker agent\n",
    "    3. Stores the selected email in the contacts DataFrame\n",
    "    \"\"\"\n",
    "    for index, contact in contacts.iterrows():\n",
    "        print(f\"\\nProcessing {contact['Name']}...\")\n",
    "        results = await process_contact_parallel(contact, sales_agents)\n",
    "        print(f\"Prepared {len(results)} e-mails for {contact['Name']}\")\n",
    "        emails=\"\"\n",
    "        # Print results from all agents\n",
    "        for i, result in enumerate(results):\n",
    "            emails += f\"\\n--- Agent {i+1} E-mail ---\\n {result}\"\n",
    "        #print(emails)\n",
    "        best = await select_best_email(emails, sales_picker)\n",
    "        print(f\"Selected: {best}\")\n",
    "        best_email = results[int(best)-1]\n",
    "        contacts.at[index,\"Email_Text\"] = best_email\n",
    "    \n",
    "    return contacts\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create a new agent - will pick contacts from the contact list that may be interested based on their positions.\n",
    "\n",
    "customer_picker_instructions = f\"\"\" You are a sales manager assistant bot. \n",
    "You will examine the name and positions of potential recipients for a cold email campaign and decide if they are a good fit to receive the e-mail.\n",
    "Respond only with YES or NO, without any preamble or explanation.\n",
    "The following additional context about the product being marketed in the campaign will help you make a decision:\n",
    "{brochure_text}\n",
    "\"\"\"\n",
    "\n",
    "customer_picker_agent = Agent(\n",
    "        name=\"Customer Picker Agent\",\n",
    "        instructions=customer_picker_instructions,\n",
    "        model=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "contacts['Include'] = \"-\" # Initialize column indicating inclusion in the campaign.\n",
    "\n",
    "for index,contact in contacts.iterrows():\n",
    "    customer_name = contact['Name']\n",
    "    customer_title = contact['Title']\n",
    "    message = f\"Is {customer_name} - {customer_title} of the company a good fit for the cold e-mail campaign? Respond with YES or NO only\"\n",
    "    result = Runner.run_streamed(\n",
    "        customer_picker_agent, \n",
    "        input = message\n",
    "    )\n",
    "    full_response = \"\"\n",
    "    async for event in result.stream_events():\n",
    "        if event.type == \"raw_response_event\" and isinstance(event.data, ResponseTextDeltaEvent):\n",
    "            full_response += event.data.delta\n",
    "    contacts.at[index, 'Include'] = full_response\n",
    "\n",
    "\n",
    "campaign_contacts = contacts[contacts['Include']=='YES']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(campaign_contacts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# for Jupyter notebooks:\n",
    "import nest_asyncio\n",
    "nest_asyncio.apply()\n",
    "\n",
    "# asyncio.run(execute_fully_parallel(campaign_contacts, sales_agents))\n",
    "# not using the full parallelization at the moment as it requires extra effort \n",
    "# to match e-mails to recepients for selection in the next step. \n",
    "\n",
    "campaign = asyncio.run(execute(campaign_contacts, sales_agents))\n",
    "print(campaign)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Updated notebook stops here - moved logic to sales_automation.py\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_picker = Agent(\n",
    "    name=\"sales_picker\",\n",
    "    instructions=\"You pick the best cold sales email from the given options. \\\n",
    "Imagine you are a customer and pick the one you are most likely to respond to. \\\n",
    "Do not give an explanation; reply with the selected email only.\",\n",
    "    model=\"gpt-4o-mini\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"Write a cold sales email\"\n",
    "\n",
    "with trace(\"Selection from sales people\"):\n",
    "    results = await asyncio.gather(\n",
    "        Runner.run(sales_agent1, message),\n",
    "        Runner.run(sales_agent2, message),\n",
    "        Runner.run(sales_agent3, message),\n",
    "    )\n",
    "    outputs = [result.final_output for result in results]\n",
    "\n",
    "    emails = \"Cold sales emails:\\n\\n\" + \"\\n\\nEmail:\\n\\n\".join(outputs)\n",
    "\n",
    "    best = await Runner.run(sales_picker, emails)\n",
    "\n",
    "    print(f\"Best sales email:\\n{best.final_output}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now go and check out the trace:\n",
    "\n",
    "https://platform.openai.com/traces"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 2: use of tools\n",
    "\n",
    "Now we will add a tool to the mix.\n",
    "\n",
    "Remember all that json boilerplate and the `handle_tool_calls()` function with the if logic.."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_agent1 = Agent(\n",
    "        name=\"Professional Sales Agent\",\n",
    "        instructions=instructions1,\n",
    "        model=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "sales_agent2 = Agent(\n",
    "        name=\"Engaging Sales Agent\",\n",
    "        instructions=instructions2,\n",
    "        model=\"gpt-4o-mini\",\n",
    ")\n",
    "\n",
    "sales_agent3 = Agent(\n",
    "        name=\"Busy Sales Agent\",\n",
    "        instructions=instructions3,\n",
    "        model=\"gpt-4o-mini\",\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_agent1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Steps 2 and 3: Tools and Agent interactions\n",
    "\n",
    "Remember all that boilerplate json?\n",
    "\n",
    "Simply wrap your function with the decorator `@function_tool`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@function_tool\n",
    "def send_email(body: str):\n",
    "    \"\"\" Send out an email with the given body to all sales prospects \"\"\"\n",
    "    sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY'))\n",
    "    from_email = Email(\"lchanio@echyperion.com\")  # Change to your verified sender\n",
    "    to_email = To(\"proklos+sg@gmail.com\")  # Change to your recipient\n",
    "    content = Content(\"text/plain\", body)\n",
    "    mail = Mail(from_email, to_email, \"Sales email\", content).get()\n",
    "    sg.client.mail.send.post(request_body=mail)\n",
    "    return {\"status\": \"success\"}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### This has automatically been converted into a tool, with the boilerplate json created"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's look at it\n",
    "send_email"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### And you can also convert an Agent into a tool"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tool1 = sales_agent1.as_tool(tool_name=\"sales_agent1\", tool_description=\"Write a cold sales email\")\n",
    "tool1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### So now we can gather all the tools together:\n",
    "\n",
    "A tool for each of our 3 email-writing agents\n",
    "\n",
    "And a tool for our function to send emails"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "description = \"Write a cold sales email\"\n",
    "\n",
    "tool1 = sales_agent1.as_tool(tool_name=\"sales_agent1\", tool_description=description)\n",
    "tool2 = sales_agent2.as_tool(tool_name=\"sales_agent2\", tool_description=description)\n",
    "tool3 = sales_agent3.as_tool(tool_name=\"sales_agent3\", tool_description=description)\n",
    "\n",
    "tools = [tool1, tool2, tool3, send_email]\n",
    "\n",
    "tools"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## And now it's time for our Sales Manager - our planning agent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Improved instructions thanks to student Guillermo F.\n",
    "\n",
    "instructions = \"\"\"\n",
    "You are a Sales Manager at ComplAI. Your goal is to find the single best cold sales email using the sales_agent tools.\n",
    " \n",
    "Follow these steps carefully:\n",
    "1. Generate Drafts: Use all three sales_agent tools to generate three different email drafts. Do not proceed until all three drafts are ready.\n",
    " \n",
    "2. Evaluate and Select: Review the drafts and choose the single best email using your judgment of which one is most effective.\n",
    " \n",
    "3. Use the send_email tool to send the best email (and only the best email) to the user.\n",
    " \n",
    "Crucial Rules:\n",
    "- You must use the sales agent tools to generate the drafts — do not write them yourself.\n",
    "- You must send ONE email using the send_email tool — never more than one.\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "sales_manager = Agent(name=\"Sales Manager\", instructions=instructions, tools=tools, model=\"gpt-4o-mini\")\n",
    "\n",
    "message = \"Send a cold sales email addressed to 'Dear CEO'\"\n",
    "\n",
    "with trace(\"Sales manager\"):\n",
    "    result = await Runner.run(sales_manager, message)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/stop.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#ff7800;\">Wait - you didn't get an email??</h2>\n",
    "            <span style=\"color:#ff7800;\">With much thanks to student Chris S. for describing his issue and fixes. \n",
    "            If you don't receive an email after running the prior cell, here are some things to check: <br/>\n",
    "            First, check your Spam folder! Several students have missed that the emails arrived in Spam!<br/>Second, print(result) and see if you are receiving errors about SSL. \n",
    "            If you're receiving SSL errors, then please check out theses <a href=\"https://chatgpt.com/share/680620ec-3b30-8012-8c26-ca86693d0e3d\">networking tips</a> and see the note in the next cell. Also look at the trace in OpenAI, and investigate on the SendGrid website, to hunt for clues. Let me know if I can help!\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### And one more suggestion to send emails from student Oleksandr on Windows 11:\n",
    "\n",
    "If you are getting certificate SSL errors, then:  \n",
    "Run this in a terminal: `uv pip install --upgrade certifi`\n",
    "\n",
    "Then run this code:\n",
    "```python\n",
    "import certifi\n",
    "import os\n",
    "os.environ['SSL_CERT_FILE'] = certifi.where()\n",
    "```\n",
    "\n",
    "Thank you Oleksandr!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Remember to check the trace\n",
    "\n",
    "https://platform.openai.com/traces\n",
    "\n",
    "And then check your email!!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Handoffs represent a way an agent can delegate to an agent, passing control to it\n",
    "\n",
    "Handoffs and Agents-as-tools are similar:\n",
    "\n",
    "In both cases, an Agent can collaborate with another Agent\n",
    "\n",
    "With tools, control passes back\n",
    "\n",
    "With handoffs, control passes across\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "subject_instructions = \"You can write a subject for a cold sales email. \\\n",
    "You are given a message and you need to write a subject for an email that is likely to get a response.\"\n",
    "\n",
    "html_instructions = \"You can convert a text email body to an HTML email body. \\\n",
    "You are given a text email body which might have some markdown \\\n",
    "and you need to convert it to an HTML email body with simple, clear, compelling layout and design.\"\n",
    "\n",
    "subject_writer = Agent(name=\"Email subject writer\", instructions=subject_instructions, model=\"gpt-4o-mini\")\n",
    "subject_tool = subject_writer.as_tool(tool_name=\"subject_writer\", tool_description=\"Write a subject for a cold sales email\")\n",
    "\n",
    "html_converter = Agent(name=\"HTML email body converter\", instructions=html_instructions, model=\"gpt-4o-mini\")\n",
    "html_tool = html_converter.as_tool(tool_name=\"html_converter\",tool_description=\"Convert a text email body to an HTML email body\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@function_tool\n",
    "def send_html_email(subject: str, html_body: str) -> Dict[str, str]:\n",
    "    \"\"\" Send out an email with the given subject and HTML body to all sales prospects \"\"\"\n",
    "    sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY'))\n",
    "    from_email = Email(\"lchanio@echyperion.com\")  # Change to your verified sender\n",
    "    to_email = To(\"proklos+sg@gmail.com\")  # Change to your recipient\n",
    "    content = Content(\"text/html\", html_body)\n",
    "    mail = Mail(from_email, to_email, subject, content).get()\n",
    "    sg.client.mail.send.post(request_body=mail)\n",
    "    return {\"status\": \"success\"}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools = [subject_tool, html_tool, send_html_email]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "instructions =\"You are an email formatter and sender. You receive the body of an email to be sent. \\\n",
    "You first use the subject_writer tool to write a subject for the email, then use the html_converter tool to convert the body to HTML. \\\n",
    "Finally, you use the send_html_email tool to send the email with the subject and HTML body.\"\n",
    "\n",
    "\n",
    "emailer_agent = Agent(\n",
    "    name=\"Email Manager\",\n",
    "    instructions=instructions,\n",
    "    tools=tools,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    handoff_description=\"Convert an email to HTML and send it\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Now we have 3 tools and 1 handoff"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tools = [tool1, tool2, tool3]\n",
    "handoffs = [emailer_agent]\n",
    "print(tools)\n",
    "print(handoffs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Improved instructions thanks to student Guillermo F.\n",
    "\n",
    "sales_manager_instructions = \"\"\"\n",
    "You are a Sales Manager at ComplAI. Your goal is to find the single best cold sales email using the sales_agent tools.\n",
    " \n",
    "Follow these steps carefully:\n",
    "1. Generate Drafts: Use all three sales_agent tools to generate three different email drafts. Do not proceed until all three drafts are ready.\n",
    " \n",
    "2. Evaluate and Select: Review the drafts and choose the single best email using your judgment of which one is most effective.\n",
    "You can use the tools multiple times if you're not satisfied with the results from the first try.\n",
    " \n",
    "3. Handoff for Sending: Pass ONLY the winning email draft to the 'Email Manager' agent. The Email Manager will take care of formatting and sending.\n",
    " \n",
    "Crucial Rules:\n",
    "- You must use the sales agent tools to generate the drafts — do not write them yourself.\n",
    "- You must hand off exactly ONE email to the Email Manager — never more than one.\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "sales_manager = Agent(\n",
    "    name=\"Sales Manager\",\n",
    "    instructions=sales_manager_instructions,\n",
    "    tools=tools,\n",
    "    handoffs=handoffs,\n",
    "    model=\"gpt-4o-mini\")\n",
    "\n",
    "message = \"Send out a cold sales email addressed to Dear CEO from Alice\"\n",
    "\n",
    "with trace(\"Automated SDR\"):\n",
    "    result = await Runner.run(sales_manager, message)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Remember to check the trace\n",
    "\n",
    "https://platform.openai.com/traces\n",
    "\n",
    "And then check your email!!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/exercise.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#ff7800;\">Exercise</h2>\n",
    "            <span style=\"color:#ff7800;\">Can you identify the Agentic design patterns that were used here?<br/>\n",
    "            What is the 1 line that changed this from being an Agentic \"workflow\" to \"agent\" under Anthropic's definition?<br/>\n",
    "            Try adding in more tools and Agents! You could have tools that handle the mail merge to send to a list.<br/><br/>\n",
    "            HARD CHALLENGE: research how you can have SendGrid call a Callback webhook when a user replies to an email,\n",
    "            Then have the SDR respond to keep the conversation going! This may require some \"vibe coding\" 😂\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/business.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#00bfff;\">Commercial implications</h2>\n",
    "            <span style=\"color:#00bfff;\">This is immediately applicable to Sales Automation; but more generally this could be applied to  end-to-end automation of any business process through conversations and tools. Think of ways you could apply an Agent solution\n",
    "            like this in your day job.\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Extra note:\n",
    "\n",
    "Google has released their Agent Development Kit (ADK). It's not yet got the traction of the other frameworks on this course, but it's getting some attention. It's interesting to note that it looks quite similar to OpenAI Agents SDK. To give you a preview, here's a peak at sample code from ADK:\n",
    "\n",
    "```\n",
    "root_agent = Agent(\n",
    "    name=\"weather_time_agent\",\n",
    "    model=\"gemini-2.0-flash\",\n",
    "    description=\"Agent to answer questions about the time and weather in a city.\",\n",
    "    instruction=\"You are a helpful agent who can answer user questions about the time and weather in a city.\",\n",
    "    tools=[get_weather, get_current_time]\n",
    ")\n",
    "```\n",
    "\n",
    "Well, that looks familiar!\n",
    "\n",
    "And a student has contributed a customer care agent in community_contributions that uses ADK."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "agents",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
